#include "Renderer.hpp"
#include <cassert>
#include <GL/glew.h>

#include "../World.hpp"
#include "../light/Light.hpp"
#include "../camera/Camera.hpp"
#include "../core/Time.hpp"
#include "../light/ShaderProgram.hpp"
#include "../core/Texture.hpp"
#include "../object/GameObject.hpp"

Renderer::Renderer(sf::Window* aWindow)
    :	window(aWindow), program(NULL) {
}

Renderer::~Renderer() {
    delete program;
}

void Renderer::use(ShaderProgram* aProgram) {
    program = aProgram;
    program->use(); // this is the default program
    findLocations(); // find where to put the uniforms to
}

void Renderer::setProjection(glm::mat4 aProjection) {
    projection = aProjection; // cache P matrix
}

void Renderer::setView(glm::mat4 aView) {
    view = aView; // cache V matrix
}

glm::mat4 Renderer::getView() {
    return view;
}

void Renderer::setModel(glm::mat4 aModel) {
    model = aModel; // cache M matrix
}

void Renderer::setLight(glm::vec3 aLight) {
    light = aLight; // cache light prosition for positional light
}

void Renderer::setColorMap(Texture* aColorMap) {   // for single texture at a time, otherwise use activeTexture
    colorMap = aColorMap->getId();
}

void Renderer::setTime(float aTime) {
    time = aTime; // set time for use in vs and fs
}

void Renderer::drawMesh(Mesh* mesh) {  // size is count of indices
    // set all uniforms
    glUniform1f(timeLoc, time);
    glUniformMatrix4fv(projectionLoc, 1, GL_FALSE, glm::value_ptr(projection));
    glUniformMatrix4fv(viewLoc, 1, GL_FALSE, glm::value_ptr(view));
    glUniformMatrix4fv(modelLoc, 1, GL_FALSE, glm::value_ptr(model));
    glUniform3fv(lightLoc, 1, glm::value_ptr(light));
    glBindTexture(GL_TEXTURE_2D, colorMap);
    glUniform1i(colorMapLoc, 0);

    // set attributes
    // set indices attribute
    glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, mesh->getIndicesBuffer());
    // set vertexattribute
    glEnableVertexAttribArray(verticesLoc);   // enable variable
    glBindBuffer(GL_ARRAY_BUFFER, mesh->getVerticesBuffer());   // make vertices the current buffer
    glVertexAttribPointer(verticesLoc, 3, GL_FLOAT, GL_FALSE, 0, 0);   // point to bound buffer
    // set normalAttribute
    glEnableVertexAttribArray(normalsLoc);
    glBindBuffer(GL_ARRAY_BUFFER, mesh->getNormalsBuffer());   // make vertices the current buffer
    glVertexAttribPointer(normalsLoc, 3, GL_FLOAT, GL_TRUE, 0, 0);   // GL_TRUE for nomalisation
    // set uv attribute
    glEnableVertexAttribArray(uvsLoc);
    glBindBuffer(GL_ARRAY_BUFFER, mesh->getUvsBuffer());   // make vertices the current buffer
    glVertexAttribPointer(uvsLoc, 2, GL_FLOAT, GL_FALSE, 0, 0);
    // draw indexed arrays
    glDrawElements(GL_TRIANGLES, mesh->size(), GL_UNSIGNED_INT, (GLvoid*) 0);
    // no current buffer, to avoid mishaps
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glDisableVertexAttribArray(uvsLoc);
    glDisableVertexAttribArray(normalsLoc);
    glDisableVertexAttribArray(verticesLoc);
    glBindTexture(GL_TEXTURE_2D, 0);
}


/*********************************************************************************/

// private members
void Renderer::findLocations() {
    assert(program != NULL);    // only if there is a program set

    projectionLoc 	= program->getUniformLocation("projection");
    viewLoc			= program->getUniformLocation("view");
    modelLoc 		= program->getUniformLocation("model");
    lightLoc 		= program->getUniformLocation("light");
    timeLoc			= program->getUniformLocation("time");
    colorMapLoc		= program->getUniformLocation("colorMap");

    verticesLoc		= program->getAttribLocation("vertex");
    normalsLoc		= program->getAttribLocation("normal");
    uvsLoc 			= program->getAttribLocation("uv");
}

void Renderer::visit(World* world) {
    program->use(); // make sure default shader program is used
    glEnable(GL_DEPTH_TEST);
    glEnable(GL_CULL_FACE);   // defaul GL_BACK
    glClearColor(0.1f, 0.1f, 0.1f, 1.0f);
    glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);   // clear screen 0
    time = Time::getInstance()->now(); // provide the shader with time float in seconds, for later use !
    model = glm::mat4(1);

    setModel(world->getTransform());

    visit(Global::getCamera());
    visit((GameObject*) world);
}

void Renderer::visit(Light* light) {
    setLight(light->getLocation());
}

void Renderer::visit(Camera* camera) {
    setProjection(camera->getProjection());   // model = cam to worldspace so inverse for world->camspace
    setView(glm::inverse(camera->getTransform()));     // model = cam to worldspace so inverse for world->camspace
}

void Renderer::visit(GameObject* object) {
    glm::mat4 modelTransform = model * object->getTransform();

    if(object->getMesh()) {   // if there is something to draw
        setModel(modelTransform);  // combine parents transfor with own

        if(object->getColorMap() != NULL) {  //is has a colormap
            setColorMap(object->getColorMap());
        }

        drawMesh(object->getMesh());
    }

    for(std::vector< GameObject*>::iterator i = object->getChildren()->begin(); i != object->getChildren()->end(); ++i) {
        setModel(modelTransform);
        (*i)->accept(this);
    }
}

void Renderer::visit(Collider* collider) {

}

void Renderer::visit(Asteroid* asteroid) {
    visit((GameObject*) asteroid);
}

void Renderer::visit(Rocket* rocket) {
    visit((GameObject*) rocket);
}

void Renderer::visit(Explosion* explosion) {
    visit((GameObject*)explosion);
}
